/**
 * @Date: Feb 26, 2010 1:29:59 PM
 */
package com.philip.journal.core.dao.spring.hibernate;

//import static com.philip.core.WarningType.UNCHECKED;
import static com.philip.journal.core.dao.DaoConstant.LIST;
import static com.philip.journal.core.dao.DaoConstant.UNIQUE;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.MappingException;
import org.hibernate.PropertyValueException;
import org.hibernate.QueryException;
import org.hibernate.Session;
import org.hibernate.TransientObjectException;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException;
import org.springframework.orm.hibernate3.HibernateSystemException;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import com.philip.core.WarningType;
import com.philip.journal.common.BeanUtils;
import com.philip.journal.core.Messages;
import com.philip.journal.core.Messages.Error;
import com.philip.journal.core.bean.AbstractBean;
import com.philip.journal.core.exception.JournalException;

/**
 * @param <E> - AbstractBean to which this DAO subclass will act.
 *
 * @author cry30
 */
public abstract class BaseDAOSpringHibernate<E extends AbstractBean> extends HibernateDaoSupport { // NOPMD by r39 on 3/30/11 12:06 PM

    /** This field is for subclass use. Will need to define class level logger to trace this class. */
    private final Log logger = LogFactory.getLog(BaseDAOSpringHibernate.class); // NOPMD by r39 on 3/30/11 12:02 PM

    /** Generic type to which this instance is based. */
    private transient Class<E> type;

    /**
     * @param pType the type to set.
     * @return this instance.
     */
    protected BaseDAOSpringHibernate<E> setType(final Class<E> pType)
    {
        this.type = pType;
        return this;
    }
    /**
     * @param <T> The journal bean as subject of this DAO.
     *
     *            Factory method for unit testing.
     *
     * @param targetClass Test target Entity class.
     * @return BaseDAOSpringHibernate instance.
     */
    public static <T extends AbstractBean> BaseDAOSpringHibernate<T> newInstance(final Class<T> targetClass)
    {
        return new BaseDAOSpringHibernate<T>() {}.setType(targetClass);
    }

    /** Currently supported sql criteria types. */
    protected enum CriteriaType {
        /** RTFC. */
        Equal, NotEqual, Like, Ilike
    };

    /** Class logger. */
    private static final Logger LOGGER = Logger.getLogger(BaseDAOSpringHibernate.class); // NOPMD by r39 on 3/30/11 12:06 PM

    /**
     * Deletes all specified object.
     *
     * @param listObj - the list of objects to delete.
     *
     * @exception IllegalArgumentException If any of the following is true:
     *                <ul>
     *                <li>when the object passed is null.
     *                <li>when one of the items in the listObj is null or is not a valid Branch.
     *                </ul>
     *
     * @exception JournalException when delete fails due to missing object for delete due to concurrency.
     */
    protected void deleteAll0(final List<?> listObj)
    {
        if (listObj == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }
        for (final Object object : listObj) {
            if (object == null) {
                throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
            }
        }

        final HibernateTemplate hibernateTemplate = getHibernateTemplate();
        try {
            hibernateTemplate.deleteAll(listObj);
            hibernateTemplate.flush();
        } catch (final HibernateOptimisticLockingFailureException holfe) {
            throw new JournalException(Messages.Error.JE_OBJ_MISS_ERR, holfe);
        } catch (final DataIntegrityViolationException exception) {
            throw new JournalException(exception.getMessage(), exception.getCause().getCause());//NOPMD Wrap internal exception with client exception.
        }

    }
    /**
     * Reads all objects.
     *
     * @return List of all objects.
     */
    @SuppressWarnings(WarningType.UNCHECKED)
    protected List<E> readAll0()
    {
        return getHibernateTemplate().<List<E>> execute(new HibernateCallback<List<E>>() {

            @Override
            public List<E> doInHibernate(final Session session) throws SQLException
            {

                return session.createCriteria(getTargetClass()).list();
            }
        });
    }

    /**
     * Reads all records whose property matches the given value. String comparison is case sensitive.
     *
     * @param property property name.
     * @param value property value.
     * @return List of entities matching the given property name-value.
     * @exception JournalException when the property passed is null;
     */
    protected List<E> readAll0(final String property, final Object value)
    {
        if (property == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }

        final Map<String, Object> param = new HashMap<String, Object>();
        param.put(property, value);
        return this.<List<E>> readObject(param, LIST, CriteriaType.Equal);
    }

    /**
     * returns List of objects with ordering.
     *
     * @param orderCriteria - String array of colon (:) separated values. (e.g. name:desc, startDate:asc)
     * @return Sorted list of objects.
     */
    @SuppressWarnings(WarningType.UNCHECKED)
    protected List<E> readAll0(final String[] orderCriteria)
    {
        final Criteria criteria = getSession().createCriteria(getTargetClass());

        String[] tokens;
        final Class<String>[] strClsArr = new Class[] { String.class };
        final Object[] objParam = new Object[1];

        for (final String string : orderCriteria) {
            tokens = string.split(":");
            Order order;
            try {
                objParam[0] = tokens[0];
                order = (Order) Order.class.getMethod(tokens[1], strClsArr).invoke(null, objParam);
            } catch (final Exception e) {
                throw new JournalException(e.getMessage(), e);
            }
            criteria.addOrder(order);
        }

        final List<E> retval = new ArrayList<E>();
        @SuppressWarnings(WarningType.RAW)
        final List list = criteria.list();
        for (final Object object : list) {
            retval.add((E) object);
        }
        return retval;
    }

    /**
     * Deletes the given persistent object.
     *
     * @param object Entity instance to delete.
     * @exception JournalException when the data to be delete is not found. This can happen due to concurrency or
     *                passing incorrect ID.
     */
    protected void delete(final Object object)
    {
        if (object == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(
                    "Developer Error.  Cannot delete null object"));
        }

        final HibernateTemplate hibernateTemplate = getHibernateTemplate();

        long pkeyId;
        final String fieldName = (String) BeanUtils.getProperty(object, "primaryKeyField");
        pkeyId = (Long) BeanUtils.getProperty(object, fieldName);

        if (hibernateTemplate.get(getTargetClass(), pkeyId) == null) {
            throw new JournalException("Error deleting non-existent entity.");
        }

        hibernateTemplate.delete(object);
        hibernateTemplate.flush();
    }

    /**
     * This will provide this abstract class to use the correct data object. CCE try-catch is a workaround with spring
     * to explicitly pass the type since it could not be determined at runtime.
     *
     * @return Target Entity class.
     */
    @SuppressWarnings(WarningType.UNCHECKED)
    protected Class<E> getTargetClass()
    {
        Class<E> retval = null;
        if (this.type == null) {
            retval = (Class<E>) ((java.lang.reflect.ParameterizedType) getClass().getGenericSuperclass())
                    .getActualTypeArguments()[0];
        } else {
            retval = this.type;
        }
        return retval;
    }

    /**
     * Reads all objects matching a list of criteria. Criteria list will need to match all specified criterion.
     *
     * @param param - Mapping of property to value for the object match.
     * @return List of objects that matches all the property-value param.
     */
    protected List<E> readAll(final Map<String, Object> param)
    {
        return this.<List<E>> readObject(param, LIST);
    }

    /**
     * Reads all objects matching (using LIKE operator) a list of criteria. Criteria list will need to match all
     * specified criterion.
     *
     * @param param - Mapping of property to value for the object match.
     * @return List of objects that matches all the property-value param.
     */
    protected List<E> readAllLike(final Map<String, String> param)
    {
        return this.<List<E>> readObject(convertStrStrMapToStrObj(param), LIST, CriteriaType.Like);
    }

    /**
     * Helper method to convert Map<String, String> to Map<String, Object>.
     *
     * @param mapStrStr input.
     * @return converted Map.
     */
    private Map<String, Object> convertStrStrMapToStrObj(final Map<String, String> mapStrStr)
    {
        final Map<String, Object> newParam = new HashMap<String, Object>();
        for (final String next : mapStrStr.keySet()) {
            final Object val = mapStrStr.get(next);
            newParam.put(next, val == null ? null : val.toString());
        }
        return newParam;
    }

    /**
     * Reads all objects matching (using LIKE operator case insensitive) a list of criteria. Criteria list will need to
     * match all specified criterion.
     *
     * @param param - Mapping of property to value for the object match.
     * @return List of objects that matches all the property-value param.
     */
    protected List<E> readAllIlike(final Map<String, String> param)
    {
        return this.<List<E>> readObject(convertStrStrMapToStrObj(param), LIST, CriteriaType.Ilike);
    }

    /**
     * @param parentBeanProp - current property name that refers to the parent bean
     * @param parentProp - property name of the parent bean to match
     * @param parentPropValue - value to match in the parentProperty parameter
     * @return List of all domain object matching the specified parameters.
     * @exception IllegalArgumentException when the parentPropertyValue is relationship-mapped hibernate class;
     */
    @SuppressWarnings(WarningType.UNCHECKED)
    protected List<E> readAllByParent(final String parentBeanProp, final String parentProp, final Object parentPropValue)
    {
        return getHibernateTemplate().<List<E>> execute(new HibernateCallback<List<E>>() {
            @Override
            public List<E> doInHibernate(final Session session) throws SQLException
            {
                final Criteria crit = session.createCriteria(getTargetClass());
                crit.createCriteria(parentBeanProp).add(Restrictions.eq(parentProp, parentPropValue));
                try {
                    final List<?> ret = crit.list();
                    final List<E> castedList = new ArrayList<E>();
                    for (final Object nextObj : ret) {
                        castedList.add((E) nextObj);
                    }
                    return castedList;
                } catch (final TransientObjectException toe) {
                    throw new IllegalArgumentException("Mapping backed property cannot be used.", toe);
                }
            }
        });
    }

    /**
     * Reads the first object matching a list of criteria. Criteria list will need exactly match all specified
     * criterion.
     *
     * @param param - Mapping of property to value for the object match.
     * @return The first object that matched all the property-value param. null if no match is found.
     * @exception JournalException when the parameter passed is null or when the query results in more than one record.
     */
    protected E readObject(final Map<String, Object> param)
    {
        if (param == null) {
            throw JournalException.wrapperException(new IllegalArgumentException("Map parameter cannot be null."));
        }
        return this.<E> readObject(param, UNIQUE);
    }

    /**
     * Reads the first object matching the named query. <br/>
     *
     * @param namedQuery - named query defined in one of the mapping configurations.
     * @param map criteria map to match.
     * @exception JournalException If any of the following is true:
     *                <ul>
     *                <li>when the namedQuery or map parameter passed is null.
     *                <li>when there is mismatch on the object type of a given property in the map.
     *                </ul>
     *
     * @return Entity object matching the named query and satisfying the criteria map.
     */
    protected E readObject(final String namedQuery, final Map<String, Object> map)
    {
        if (namedQuery == null || map == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }

        try {
            final HibernateCallback<E> callBack = new ReadObjectCallback0<E>(namedQuery, map);
            return getHibernateTemplate().execute(callBack);
        } catch (final HibernateSystemException hse) {
            if (hse.getCause() instanceof MappingException) {
                throw JournalException.wrapperException(new IllegalArgumentException("Invalid namedQuery: "
                        + namedQuery, hse));
            } else {
                throw JournalException.wrapperException(hse);
            }
        }
    }
    /**
     * Reusable method to find the first matching object given a property and the exact value to match.
     *
     * @param property - target bean property to match with.
     * @param value - value to match.
     * @return The first matching object. null if no match was found.
     * @exception JournalException If any of the following is true:
     *                <ul>
     *                <li> <code>value</code> did not match the bean field type.</li>
     *                <li> <code>property</code> is null.</li>
     *                </ul>
     */
    protected E readObject(final String property, final Object value)
    {
        if (property == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Error.IAE_NULL));
        }

        final Map<String, Object> param = new HashMap<String, Object>();
        param.put(property, value);
        return this.<E> readObject(param, UNIQUE, CriteriaType.Equal);
    }

    /**
     * Retrieves a bean matching a referenced master Object along with a matching property for the bean.
     *
     * @param parentBeanProp - bean property name of which the type is the master bean.
     * @param parentProp - property name of the master bean.
     * @param parentPropValue - property value of the master object.
     * @param beanProperty - ordinary property name of the bean
     * @param beanValue - value of the beanProp param.
     *
     * @exception NullPointerException If any of the following is true:
     *                <ul>
     *                <li><code>parentBeanProperty</code> is <code>null</code>.
     *                <li><code>parentProperty</code> is <code>null</code>.
     *                <li><code>beanProperty</code> is <code>null</code>.
     *                </ul>
     * @exception JournalException If any of the following is true:
     *                <ul>
     *                <li><code>parentBeanProp</code>could not be resolved.
     *                <li><code>parentProp</code>could not be resolved.
     *                <li><code>beanProperty</code>could not be resolved.
     *                <li><code>parentPropertyValue</code> did not match the parent bean field type.
     *                <li><code>beanValue</code> did not match the bean field type.
     *
     *                </ul>
     * @return the matched bean. null if it cannot be found.
     */
    @SuppressWarnings(com.philip.core.WarningType.UNCHECKED)
    protected E readObjectByParent(final String parentBeanProp, final String parentProp, final Object parentPropValue,
            final String beanProperty, final Object beanValue)
    {

        return getHibernateTemplate().execute(new HibernateCallback<E>() {
            @Override
            public E doInHibernate(final Session session) throws SQLException
            {
                final Criteria crit = session.createCriteria(getTargetClass());
                crit.createCriteria(parentBeanProp).add(Restrictions.eq(parentProp, parentPropValue));
                crit.add(Restrictions.eq(beanProperty, beanValue));
                try {
                    return (E) crit.uniqueResult();
                } catch (final QueryException qe) {
                    throw JournalException.wrapperException(new IllegalArgumentException(qe.getMessage(), qe));
                } catch (final NullPointerException qe) { // NOPMD by r39 - Hibernate internal design throws the NullPointerException.
                    throw JournalException.wrapperException(new IllegalArgumentException(qe.getMessage(), qe));
                } catch (final ClassCastException qe) {
                    throw JournalException.wrapperException(new IllegalArgumentException(qe.getMessage(), qe));
                }
            }
        });
    }

    /**
     * Insert or update the given object.
     *
     * @param object - hibernate mapped entity.
     * @exception JournalException If any of the following is true:
     *                <ul>
     *                <li><code>object</code> passed has a non-existent primary key value.
     *                <li>null value on a not-null property constraint.
     *                </ul>
     *
     * @exception JournalException when the object passed is null.
     */
    protected void save(final E object)
    {
        if (object == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }

        try {
            final HibernateTemplate hibernateTemplate = getHibernateTemplate();
            hibernateTemplate.saveOrUpdate(object);
            hibernateTemplate.flush();
        } catch (final HibernateSystemException hse) {
            LOGGER.error(hse.getMessage());
        } catch (final DataAccessException dae) {
            final Throwable cause = dae.getCause();
            if (cause.getClass().equals(org.hibernate.StaleStateException.class)) {
                throw new JournalException(Messages.Error.IAE_GENERIC, Messages.Error.IAE_INV_UPDATE,
                        cause.getMessage(), dae);
            } else if (cause.getClass().equals(PropertyValueException.class)) {
                throw new JournalException(cause.getMessage(), cause); // NOPMD by r39
            } else if (cause.getClass().equals(TransientObjectException.class)) {
                throw new JournalException(Messages.Error.JE_UNSVD_ROBJ_ERR, cause); // NOPMD by
            } else {
                final String errorMsg = ((SQLException) dae.getCause().getCause()).getMessage();
                throw new JournalException(errorMsg, dae);
            }
        } catch (final Exception e) {
            throw new JournalException(e.getMessage(), e);
        }
    }
    /**
     * Helper method to read a List or a single object matching the name-value criteria.
     *
     * @param <T> Entity type or List of Entity type.
     * @param param - name value pair to match for.
     * @param methodName - specify the method to use for the result whether list or uniqueResult.
     * @param criteriaType Criteria type based on enumerated supported criterias.
     * @return List or a single object matching the name-value criteria depending on the specified methodName.
     */
    private <T> T readObject(final Map<String, Object> param, final String methodName, final CriteriaType criteriaType)
    {
        return getHibernateTemplate().execute(
                new ReadObjectCallback1<T>(param, methodName, criteriaType, getTargetClass()));
    }

    /**
     * Helper method to read a List or a single object matching the name-value criteria. Defaults to use Equal operator.
     *
     * @param <T> Entity type or List of Entity type.
     * @param param - name value pair to match for.
     * @param methodName - specify the method to use for the result whether list or uniqueResult.
     * @return List or a single object matching the name-value criteria depending on the specified methodName.
     */
    private <T> T readObject(final Map<String, Object> param, final String methodName)
    {
        return this.<T> readObject(param, methodName, CriteriaType.Equal);
    }

    /**
     * Accessor method.
     *
     * @return the logger
     */
    public Log getLogger()
    {
        return logger;
    }

}
